#include "CodeEditorPrivate.h"

#include "FileDocument.h"
#include "CodeDocumentLayout.h"
#include "CodeEditorOverlay.h"

namespace CodeEdit {
namespace Internal {

/**
 * @static
 * Minimum & maximum font zoom (ratio in percent) allowed.
 */
int CodeEditorPrivate::fontZoomMin = 20;
int CodeEditorPrivate::fontZoomMax = 500;

CodeEditorPrivate::CodeEditorPrivate()
    : fontZoom(100)
    , lastScrollPos(-1)
    , lineNumber(-1)
    , contentsChanged(false)
    , lastCursorChangeWasInteresting(false)
    , document(new FileDocument)
    , parenthesesMatchingEnabled(false)
    , autoParenthesesEnabled(true)
    , extraArea(0)
    , mouseOnFoldedMarker(false)
    , marksVisible(true)
    , codeFoldingVisible(true)
    , codeFoldingSupported(true)
    , revisionsVisible(true)
    , lineNumbersVisible(true)
    , highlightCurrentLine(true)
    , requestMarkEnabled(true)
    , lineSeparatorsAllowed(false)
    , visibleWrapColumn(0)
    , actionHack(0)
    , inBlockSelectionMode(false)
    , lastEventWasBlockSelectionEvent(false)
    , blockSelectionExtraX(0)
    , moveLineUndoHack(false)
    , cursorBlockNumber(-1)
{
}

CodeEditorPrivate::~CodeEditorPrivate()
{
}

void CodeEditorPrivate::setupDocumentSignals(FileDocument *document)
{
    FileDocument *oldDocument = q->fileDocument();
    if (oldDocument) {
        q->disconnect(oldDocument->document(), 0, q, 0);
        q->disconnect(oldDocument, 0, q, 0);
    }

    QTextDocument *doc = document->document();
    CodeDocumentLayout *docLayout = qobject_cast<CodeDocumentLayout*>(doc->documentLayout());
    if (!docLayout) {
        QTextOption opt = doc->defaultTextOption();
        opt.setTextDirection(Qt::LeftToRight);
        opt.setFlags(opt.flags() | QTextOption::IncludeTrailingSpaces
                   | QTextOption::AddSpaceForLineAndParagraphSeparators);
        doc->setDefaultTextOption(opt);
        docLayout = new CodeDocumentLayout(doc);
        doc->setDocumentLayout(docLayout);
    }

    q->setDocument(doc);
    QObject::connect(docLayout, SIGNAL(updateBlock(QTextBlock)), q, SLOT(slotUpdateBlockNotify(QTextBlock)));
    QObject::connect(q, SIGNAL(requestBlockUpdate(QTextBlock)), docLayout, SIGNAL(updateBlock(QTextBlock)));
    QObject::connect(doc, SIGNAL(modificationChanged(bool)), q, SIGNAL(changed()));
    QObject::connect(doc, SIGNAL(contentsChange(int,int,int)), q,
        SLOT(editorContentsChanged(int,int,int)), Qt::DirectConnection);
    QObject::connect(document, SIGNAL(changed()), q, SIGNAL(changed()));
    QObject::connect(document, SIGNAL(titleChanged(QString)), q, SLOT(setDisplayName(const QString &)));
    QObject::connect(document, SIGNAL(aboutToReload()), q, SLOT(memorizeCursorPosition()));
    QObject::connect(document, SIGNAL(reloaded()), q, SLOT(restoreCursorPosition()));
    q->slotUpdateExtraAreaWidth();
}

/// Utilized by CodeEditorPrivate::print()
static void printPage(int index, QPainter *painter, const QTextDocument *doc,
                      const QRectF &body, const QRectF &titleBox, const QString &title)
{
    painter->save();

    painter->translate(body.left(), body.top() - (index - 1) * body.height());
    QRectF view(0, (index - 1) * body.height(), body.width(), body.height());

    QAbstractTextDocumentLayout *layout = doc->documentLayout();
    QAbstractTextDocumentLayout::PaintContext ctx;

    painter->setFont(QFont(doc->defaultFont()));
    QRectF box = titleBox.translated(0, view.top());
    int dpix = painter->device()->logicalDpiX();
    int dpiy = painter->device()->logicalDpiY();
    int mx = 5 * dpix / 72.0;
    int my = 2 * dpiy / 72.0;
    painter->fillRect(box.adjusted(-mx, -my, mx, my), QColor(210, 210, 210));
    if (!title.isEmpty())
        painter->drawText(box, Qt::AlignCenter, title);
    const QString pageString = QString::number(index);
    painter->drawText(box, Qt::AlignRight, pageString);

    painter->setClipRect(view);
    ctx.clip = view;
    // don't use the system palette text as default text color, on HP/UX
    // for example that's white, and white text on white paper doesn't
    // look that nice
    ctx.palette.setColor(QPalette::Text, Qt::black);

    layout->draw(painter, ctx);

    painter->restore();
}

void CodeEditorPrivate::print(QPrinter *printer)
{
    QTextDocument *doc = q->document();

    QString title = q->displayName();
    if (title.isEmpty())
        printer->setDocName(title);

    QPainter p(printer);

    // Check that there is a valid device to print to.
    if (!p.isActive())
        return;

    doc = doc->clone(doc);

    QTextOption opt = doc->defaultTextOption();
    opt.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
    doc->setDefaultTextOption(opt);

    (void)doc->documentLayout(); // make sure that there is a layout

    QColor background = q->palette().color(QPalette::Base);
    bool backgroundIsDark = background.value() < 128;

    for (QTextBlock srcBlock = q->document()->firstBlock(), dstBlock = doc->firstBlock();
         srcBlock.isValid() && dstBlock.isValid();
         srcBlock = srcBlock.next(), dstBlock = dstBlock.next()) {

        QList<QTextLayout::FormatRange> formatList = srcBlock.layout()->additionalFormats();
        if (backgroundIsDark) {
            // adjust syntax highlighting colors for better contrast
            for (int i = formatList.count() - 1; i >=0; --i) {
                QTextCharFormat &format = formatList[i].format;
                if (format.background().color() == background) {
                    QBrush brush = format.foreground();
                    QColor color = brush.color();
                    int h,s,v,a;
                    color.getHsv(&h, &s, &v, &a);
                    color.setHsv(h, s, qMin(128, v), a);
                    brush.setColor(color);
                    format.setForeground(brush);
                }
                format.setBackground(Qt::white);
            }
        }

        dstBlock.layout()->setAdditionalFormats(formatList);
    }

    QAbstractTextDocumentLayout *layout = doc->documentLayout();
    layout->setPaintDevice(p.device());

    int dpiy = p.device()->logicalDpiY();
    int margin = (int) ((2/2.54)*dpiy); // 2 cm margins

    QTextFrameFormat fmt = doc->rootFrame()->frameFormat();
    fmt.setMargin(margin);
    doc->rootFrame()->setFrameFormat(fmt);

    QRectF pageRect(printer->pageRect());
    QRectF body = QRectF(0, 0, pageRect.width(), pageRect.height());
    QFontMetrics fontMetrics(doc->defaultFont(), p.device());

    QRectF titleBox(margin,
                    body.top() + margin
                    - fontMetrics.height()
                    - 6 * dpiy / 72.0,
                    body.width() - 2*margin,
                    fontMetrics.height());
    doc->setPageSize(body.size());

    int docCopies;
    int pageCopies;
    if (printer->collateCopies() == true){
        docCopies = 1;
        pageCopies = printer->numCopies();
    } else {
        docCopies = printer->numCopies();
        pageCopies = 1;
    }

    int fromPage = printer->fromPage();
    int toPage = printer->toPage();
    bool ascending = true;

    if (fromPage == 0 && toPage == 0) {
        fromPage = 1;
        toPage = doc->pageCount();
    }
    // paranoia check
    fromPage = qMax(1, fromPage);
    toPage = qMin(doc->pageCount(), toPage);

    if (printer->pageOrder() == QPrinter::LastPageFirst) {
        int tmp = fromPage;
        fromPage = toPage;
        toPage = tmp;
        ascending = false;
    }

    for (int i = 0; i < docCopies; ++i) {

        int page = fromPage;
        while (true) {
            for (int j = 0; j < pageCopies; ++j) {
                if (printer->printerState() == QPrinter::Aborted
                    || printer->printerState() == QPrinter::Error)
                    goto UserCanceled;
                printPage(page, &p, doc, body, titleBox, title);
                if (j < pageCopies - 1)
                    printer->newPage();
            }

            if (page == toPage)
                break;

            if (ascending)
                ++page;
            else
                --page;

            printer->newPage();
        }

        if ( i < docCopies - 1)
            printer->newPage();
    }

UserCanceled:
    delete doc;
}

void CodeEditorPrivate::updateMarksLineNumber()
{
    QTextDocument *doc = q->document();
    QTextBlock block = doc->begin();
    int blockNumber = 0;
    while (block.isValid()) {
        if (const TextBlockUserData *userData = CodeDocumentLayout::testUserData(block))
            foreach (ITextMark *mrk, userData->marks()) {
                mrk->updateLineNumber(blockNumber + 1);
            }
        block = block.next();
        ++blockNumber;
    }
}

void CodeEditorPrivate::updateMarksBlock(const QTextBlock &block)
{
    if (const TextBlockUserData *userData = CodeDocumentLayout::testUserData(block))
        foreach (ITextMark *mrk, userData->marks())
            mrk->updateBlock(block);
}

void CodeEditorPrivate::highlightSearchResults(const QTextBlock &block, CodeEditorOverlay *overlay)
{
    if (searchExpr.isEmpty())
        return;

    int blockPosition = block.position();

    QTextCursor cursor = q->textCursor();
    QString text = block.text();
    text.replace(QChar::Nbsp, QLatin1Char(' '));
    int idx = -1;
    int l = 1;

    int _findScopeFirstColumn = 0;
    if (!findScopeStart.isNull() && findScopeVerticalBlockSelection)  {
        int positionInBlock = findScopeStart.position() - findScopeStart.block().position();
        _findScopeFirstColumn = positionInBlock + 1;
    }

    while (idx < text.length()) {
        idx = searchExpr.indexIn(text, idx + l);
        if (idx < 0)
            break;
        l = searchExpr.matchedLength();
        if (l == 0)
            break;
        if ((findFlags & Find::IFindSupport::FindWholeWords)
            && ((idx && text.at(idx-1).isLetterOrNumber())
                || (idx + l < text.length() && text.at(idx + l).isLetterOrNumber())))
            continue;

        if (!findScopeStart.isNull()) {
            if (blockPosition + idx < findScopeStart.position()
                || blockPosition + idx + l > findScopeEnd.position())
                continue;
            if (findScopeVerticalBlockSelection) {
                if (idx < _findScopeFirstColumn
                    || idx + l > _findScopeFirstColumn + findScopeVerticalBlockSelection)
                    continue;
            }
        }

        overlay->addOverlaySelection(blockPosition + idx,
                                     blockPosition + idx + l,
                                     searchResultFormat.background().color().darker(120),
                                     QColor(),
                                     (idx == cursor.selectionStart() - blockPosition
                                      && idx + l == cursor.selectionEnd() - blockPosition)?
                                     CodeEditorOverlay::DropShadow : 0);

    }
}

int CodeEditorPrivate::visualIndent(const QTextBlock &block) const
{
    if (!block.isValid())
        return 0;
    const QTextDocument *document = block.document();
    int i = 0;
    while (i < block.length()) {
        if (!document->characterAt(block.position() + i).isSpace()) {
            QTextCursor cursor(block);
            cursor.setPosition(block.position() + i);
            return q->cursorRect(cursor).x();
        }
        ++i;
    }

    return 0;
}

/**
 * Collapses the first comment in a file, if there is only whitespace above
 */
void CodeEditorPrivate::foldLicenseHeader()
{
    QTextDocument *doc = q->document();
    CodeDocumentLayout *docLayout = qobject_cast<CodeDocumentLayout*>(doc->documentLayout());
    if (!docLayout)
        return;
    QTextBlock block = doc->firstBlock();
    const TabSettings &ts = document->tabSettings();
    while (block.isValid() && block.isVisible()) {
        QString text = block.text();
        if (CodeDocumentLayout::canFold(block) && block.next().isVisible()) {
            if (text.trimmed().startsWith(QLatin1String("/*"))) {
                CodeDocumentLayout::doFoldOrUnfold(block, false);
                moveCursorVisible();
                docLayout->requestUpdate();
                docLayout->emitDocumentSizeChanged();
                break;
            }
        }
        if (ts.firstNonSpace(text) < text.size())
            break;
        block = block.next();
    }
}

void CodeEditorPrivate::clearBlockSelection()
{
    if (inBlockSelectionMode) {
        inBlockSelectionMode = false;
        QTextCursor cursor = q->textCursor();
        cursor.clearSelection();
        q->setTextCursor(cursor);
    }
}

QString CodeEditorPrivate::copyBlockSelection()
{
    QString text;

    QTextCursor cursor = q->textCursor();
    if (!cursor.hasSelection())
        return text;

    QTextDocument *doc = q->document();
    int start = cursor.selectionStart();
    int end = cursor.selectionEnd();
    QTextBlock startBlock = doc->findBlock(start);
    int columnA = start - startBlock.position();
    QTextBlock endBlock = doc->findBlock(end);
    int columnB = end - endBlock.position();
    int firstColumn = qMin(columnA, columnB);
    int lastColumn = qMax(columnA, columnB) + blockSelectionExtraX;

    QTextBlock block = startBlock;
    for (;;) {

        cursor.setPosition(block.position() + qMin(block.length()-1, firstColumn));
        cursor.setPosition(block.position() + qMin(block.length()-1, lastColumn), QTextCursor::KeepAnchor);
        text += cursor.selectedText();
        if (block == endBlock)
            break;
        text += QLatin1Char('\n');
        block = block.next();
    }

    return text;
}

void CodeEditorPrivate::removeBlockSelection(const QString &text)
{
    QTextCursor cursor = q->textCursor();
    if (!cursor.hasSelection())
        return;

    QTextDocument *doc = q->document();
    int start = cursor.selectionStart();
    int end = cursor.selectionEnd();
    QTextBlock startBlock = doc->findBlock(start);
    int columnA = start - startBlock.position();
    QTextBlock endBlock = doc->findBlock(end);
    int columnB = end - endBlock.position();
    int firstColumn = qMin(columnA, columnB);
    int lastColumn = qMax(columnA, columnB) + blockSelectionExtraX;

    cursor.clearSelection();
    cursor.beginEditBlock();

    QTextBlock block = startBlock;
    for (;;) {

        cursor.setPosition(block.position() + qMin(block.length()-1, firstColumn));
        cursor.setPosition(block.position() + qMin(block.length()-1, lastColumn), QTextCursor::KeepAnchor);
        cursor.removeSelectedText();
        if (block == endBlock)
            break;
        block = block.next();
    }

    cursor.setPosition(start);
    if (!text.isEmpty())
        cursor.insertText(text);
    cursor.endEditBlock();
    q->setTextCursor(cursor);
}

void CodeEditorPrivate::moveCursorVisible(bool ensureVisible)
{
    QTextCursor cursor = q->textCursor();
    if (!cursor.block().isVisible()) {
        cursor.setVisualNavigation(true);
        cursor.movePosition(QTextCursor::Up);
        q->setTextCursor(cursor);
    }
    if (ensureVisible)
        q->ensureCursorVisible();
}

void CodeEditorPrivate::clearVisibleFoldedBlock()
{
    if (suggestedVisibleFoldedBlockNumber) {
        suggestedVisibleFoldedBlockNumber = -1;
        foldedBlockTimer.stop();
    }
    if (visibleFoldedBlockNumber >= 0) {
        visibleFoldedBlockNumber = -1;
        q->viewport()->update();
    }
}

} // namespace Internal
} // namespace CodeEdit
